<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>激光文字</title>

    <style>
        .page-laser-to-text {
            position: relative;
            overflow: hidden;
        }

        .page-laser-to-text canvas {
            display: block;
            margin-top: -100px;
        }

        .page-laser-to-text input {
            position: absolute;
            bottom: 50px;
            left: 0;
            right: 0;
            display: block;
            outline: none;
            background-color: rgba(38, 50, 56, 0.2);
            color: #ffffff;
            border: none;
            width: 50%;
            min-width: 500px;
            max-width: 100%;
            margin: auto;
            height: 60px;
            line-height: 60px;
            font-size: 40px;
            padding: 0 20px;
        }

        .page-laser-to-text input:hover,
        .page-laser-to-text input:focus {
            border: 1px solid rgba(38, 50, 56, 0.6);
        }

        .page-laser-to-text input::-webkit-input-placeholder {
            color: rgba(255, 255, 255, 0.1);
        }

    </style>
</head>

<body>
    <div class="page page-laser-to-text">
        <input id="input" type="text" maxlength="24" placeholder="I love you!">
        <canvas id="canvas"></canvas>
    </div>

    <script>
        let canvas, ctx, w, h, laser, text, particles, input;

        function Laser(options) {
            options = options || {};
            this.lifespan = options.lifespan || Math.round(Math.random() * 20 + 20);
            this.maxlife = this.lifespan;
            this.color = options.color || '#fd2423';
            this.x = options.x || Math.random() * w;
            this.y = options.y || Math.random() * h;
            this.width = options.width || 2;

            this.update = function(index, array) {
                this.lifespan > 0 && this.lifespan--;
                this.lifespan <= 0 && this.remove(index, array);
            }

            this.render = function(ctx) {
                if (this.lifespan <= 0) return;
                ctx.beginPath();
                ctx.globalAlpha = this.lifespan / this.maxlife;
                ctx.strokeStyle = this.color;
                ctx.lineWidth = this.width;
                ctx.moveTo(this.x, this.y);
                ctx.lineTo(w, this.y);
                ctx.stroke();
                ctx.closePath();
            }

            this.remove = function(index, array) {
                array.splice(index, 1);
            }
        }

        function Spark(options) {
            options = options || {};
            this.x = options.x || w * 0.5;
            this.y = options.y || h * 0.5;
            this.v = options.v || {
                direct: Math.random() * Math.PI * 2,
                weight: Math.random() * 10 + 2,
                friction: 0.94
            };
            this.a = options.a || {
                change: Math.random() * 0.2 - 0.1,
                min: this.v.direct - Math.PI * 0.4,
                max: this.v.direct + Math.PI * 0.4
            };
            this.g = options.g || {
                direct: Math.PI * 0.5 + (Math.random() * 0.4 - 0.2),
                weight: Math.random() * 0.5 + 0.5
            };
            this.width = options.width || Math.random() * 3;
            this.lifespan = options.lifespan || Math.round(Math.random() * 20 + 40);
            this.maxlife = this.lifespan;
            this.color = options.color || '#fdab23';
            this.prev = {
                x: this.x,
                y: this.y
            };

            this.update = function(index, array) {
                this.prev = {
                    x: this.x,
                    y: this.y
                };
                this.x += Math.cos(this.v.direct) * this.v.weight;
                this.x += Math.cos(this.g.direct) * this.g.weight;
                this.y += Math.sin(this.v.direct) * this.v.weight;
                this.y += Math.sin(this.g.direct) * this.g.weight;
                this.v.weight *= this.v.friction;
                this.v.direct += this.a.change;
                (this.v.direct > this.a.max || this.v.direct < this.a.min) && (this.a.change *= -1);
                this.lifespan > 0 && this.lifespan--;
                this.lifespan <= 0 && this.remove(index, array);
            }

            this.render = function(ctx) {
                if (this.lifespan <= 0) return;
                ctx.beginPath();
                ctx.globalAlpha = this.lifespan / this.maxlife;
                ctx.strokeStyle = this.color;
                ctx.lineWidth = this.width;
                ctx.moveTo(this.x, this.y);
                ctx.lineTo(this.prev.x, this.prev.y);
                ctx.stroke();
                ctx.closePath();
            }

            this.remove = function(index, array) {
                array.splice(index, 1);
            }
        }

        function Particles(options) {
            options = options || {};
            this.max = options.max || Math.round(Math.random() * 20 + 10);
            this.sparks = [...new Array(this.max)].map(() => new Spark(options));

            this.update = function() {
                this.sparks.forEach((s, i) => s.update(i, this.sparks));
            }

            this.render = function(ctx) {
                this.sparks.forEach(s => s.render(ctx));
            }
        }

        function Text(options) {
            options = options || {};
            const pool = document.createElement('canvas');
            const buffer = pool.getContext('2d');
            pool.width = w;
            buffer.fillStyle = '#000000';
            buffer.fillRect(0, 0, pool.width, pool.height);

            this.size = options.size || 100;
            this.copy = (options.copy || `Hello!`) + ' ';
            this.color = options.color || '#fecd96';
            this.delay = options.delay || 4;
            this.basedelay = this.delay;
            buffer.font = `${this.size}px Comic Sans MS`;
            this.bound = buffer.measureText(this.copy);
            this.bound.height = this.size * 1.5;
            this.x = options.x || w * 0.5 - this.bound.width * 0.5;
            this.y = options.y || h * 0.5 - this.size * 0.5;

            buffer.strokeStyle = this.color;
            buffer.strokeText(this.copy, 0, this.bound.height * 0.8);
            this.data = buffer.getImageData(0, 0, this.bound.width, this.bound.height);
            this.index = 0;

            this.update = function() {
                if (this.index >= this.bound.width) {
                    this.index = 0;
                    return;
                }
                const data = this.data.data;
                for (let i = this.index * 4; i < data.length; i += (4 * this.data.width)) {
                    const bitmap = data[i] + data[i + 1] + data[i + 2] + data[i + 3];
                    if (bitmap > 255 && Math.random() > 0.86) {
                        const x = this.x + this.index;
                        const y = this.y + (i / this.bound.width / 4);
                        laser.push(new Laser({
                            x: x,
                            y: y
                        }));
                        Math.random() > 0.7 && particles.push(new Particles({
                            x: x,
                            y: y
                        }));
                    }
                }
                this.delay-- < 0 && (this.index++ && (this.delay += this.basedelay));
            }

            this.render = function(ctx) {
                ctx.putImageData(this.data, this.x, this.y, 0, 0, this.index, this.bound.height);
            }
        }

        function loop() {
            update();
            render();
            requestAnimationFrame(loop);
        }

        function update() {
            text.update();
            laser.forEach((l, i) => l.update(i, laser));
            particles.forEach(p => p.update());
        }

        function render() {
            ctx.globalCompositeOperation = 'source-over';
            ctx.globalAlpha = 0.9;
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, w, h);
            //
            ctx.globalCompositeOperation = 'screen';
            text.render(ctx);
            laser.forEach(l => l.render(ctx));
            particles.forEach(p => p.render(ctx));
        }

        (function() {
            //
            canvas = document.getElementById('canvas');
            input = document.getElementById('input');
            ctx = canvas.getContext('2d');
            w = window.innerWidth;
            h = window.innerHeight;
            canvas.width = w;
            canvas.height = h;
            laser = [];
            particles = [];
            //
            text = new Text({
                copy: 'I Love You!'
            });
            canvas.addEventListener('click', (e) => {
                const x = e.clientX;
                const y = e.clientY;
                laser.push(new Laser({
                    x: x,
                    y: y
                }));
                particles.push(new Particles({
                    x: x,
                    y: y
                }));
            });
            let cb = 0;
            input.addEventListener('keyup', (e) => {
                clearTimeout(cb);
                cb = setTimeout(() => {
                    text = new Text({
                        copy: input.value
                    });
                }, 300);
            });
            //
            loop();
        })()

    </script>

</body>

</html>
